/*
 * ***** BEGIN GPL LICENSE BLOCK *****
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * The Original Code is Copyright (C) 2016 by Mike Erwin.
 * All rights reserved.
 *
 * Contributor(s): Blender Foundation
 *
 * ***** END GPL LICENSE BLOCK *****
 */

/** \file blender/gpu/intern/gpu_element.c
 *  \ingroup gpu
 *
 * GPU element list (AKA index buffer)
 */

#include "MEM_guardedalloc.h"

#include "GPU_element.h"

#include "gpu_context_private.h"

#include <stdlib.h>

#define KEEP_SINGLE_COPY 1

static GLenum convert_index_type_to_gl(GPUIndexBufType type)
{
	static const GLenum table[] = {
		[GPU_INDEX_U8] = GL_UNSIGNED_BYTE, /* GL has this, Vulkan does not */
		[GPU_INDEX_U16] = GL_UNSIGNED_SHORT,
		[GPU_INDEX_U32] = GL_UNSIGNED_INT
	};
	return table[type];
}

uint GPU_indexbuf_size_get(const GPUIndexBuf *elem)
{
#if GPU_TRACK_INDEX_RANGE
	static const uint table[] = {
		[GPU_INDEX_U8] = sizeof(GLubyte), /* GL has this, Vulkan does not */
		[GPU_INDEX_U16] = sizeof(GLushort),
		[GPU_INDEX_U32] = sizeof(GLuint)
	};
	return elem->index_len * table[elem->index_type];
#else
	return elem->index_len * sizeof(GLuint);
#endif
}

int GPU_indexbuf_primitive_len(GPUPrimType prim_type)
{
	switch (prim_type) {
		case GPU_PRIM_POINTS:
			return 1;
		case GPU_PRIM_LINES:
			return 2;
		case GPU_PRIM_TRIS:
			return 3;
		case GPU_PRIM_LINES_ADJ:
			return 4;
		default:
			break;
	}
#if TRUST_NO_ONE
	assert(false);
#endif
	return -1;
}

void GPU_indexbuf_init_ex(
        GPUIndexBufBuilder *builder, GPUPrimType prim_type,
        uint index_len, uint vertex_len, bool use_prim_restart)
{
	builder->use_prim_restart = use_prim_restart;
	builder->max_allowed_index = vertex_len - 1;
	builder->max_index_len = index_len;
	builder->index_len = 0; // start empty
	builder->prim_type = prim_type;
	builder->data = MEM_callocN(builder->max_index_len * sizeof(uint), "GPUIndexBuf data");
}

void GPU_indexbuf_init(GPUIndexBufBuilder *builder, GPUPrimType prim_type, uint prim_len, uint vertex_len)
{
	int verts_per_prim = GPU_indexbuf_primitive_len(prim_type);
#if TRUST_NO_ONE
	assert(verts_per_prim != -1);
#endif
	GPU_indexbuf_init_ex(builder, prim_type, prim_len * (uint)verts_per_prim, vertex_len, false);
}

void GPU_indexbuf_add_generic_vert(GPUIndexBufBuilder *builder, uint v)
{
#if TRUST_NO_ONE
	assert(builder->data != NULL);
	assert(builder->index_len < builder->max_index_len);
	assert(v <= builder->max_allowed_index);
#endif
	builder->data[builder->index_len++] = v;
}

void GPU_indexbuf_add_primitive_restart(GPUIndexBufBuilder *builder)
{
#if TRUST_NO_ONE
	assert(builder->data != NULL);
	assert(builder->index_len < builder->max_index_len);
	assert(builder->use_prim_restart);
#endif
	builder->data[builder->index_len++] = GPU_PRIM_RESTART;
}

void GPU_indexbuf_add_point_vert(GPUIndexBufBuilder *builder, uint v)
{
#if TRUST_NO_ONE
	assert(builder->prim_type == GPU_PRIM_POINTS);
#endif
	GPU_indexbuf_add_generic_vert(builder, v);
}

void GPU_indexbuf_add_line_verts(GPUIndexBufBuilder *builder, uint v1, uint v2)
{
#if TRUST_NO_ONE
	assert(builder->prim_type == GPU_PRIM_LINES);
	assert(v1 != v2);
#endif
	GPU_indexbuf_add_generic_vert(builder, v1);
	GPU_indexbuf_add_generic_vert(builder, v2);
}

void GPU_indexbuf_add_tri_verts(GPUIndexBufBuilder *builder, uint v1, uint v2, uint v3)
{
#if TRUST_NO_ONE
	assert(builder->prim_type == GPU_PRIM_TRIS);
	assert(v1 != v2 && v2 != v3 && v3 != v1);
#endif
	GPU_indexbuf_add_generic_vert(builder, v1);
	GPU_indexbuf_add_generic_vert(builder, v2);
	GPU_indexbuf_add_generic_vert(builder, v3);
}

void GPU_indexbuf_add_line_adj_verts(GPUIndexBufBuilder *builder, uint v1, uint v2, uint v3, uint v4)
{
#if TRUST_NO_ONE
	assert(builder->prim_type == GPU_PRIM_LINES_ADJ);
	assert(v2 != v3); /* only the line need diff indices */
#endif
	GPU_indexbuf_add_generic_vert(builder, v1);
	GPU_indexbuf_add_generic_vert(builder, v2);
	GPU_indexbuf_add_generic_vert(builder, v3);
	GPU_indexbuf_add_generic_vert(builder, v4);
}

#if GPU_TRACK_INDEX_RANGE
/* Everything remains 32 bit while building to keep things simple.
 * Find min/max after, then convert to smallest index type possible. */

static uint index_range(const uint values[], uint value_len, uint *min_out, uint *max_out)
{
	if (value_len == 0) {
		*min_out = 0;
		*max_out = 0;
		return 0;
	}
	uint min_value = values[0];
	uint max_value = values[0];
	for (uint i = 1; i < value_len; ++i) {
		const uint value = values[i];
		if (value == GPU_PRIM_RESTART)
			continue;
		else if (value < min_value)
			min_value = value;
		else if (value > max_value)
			max_value = value;
	}
	*min_out = min_value;
	*max_out = max_value;
	return max_value - min_value;
}

static void squeeze_indices_byte(GPUIndexBufBuilder *builder, GPUIndexBuf *elem)
{
	const uint *values = builder->data;
	const uint index_len = elem->index_len;

	/* data will never be *larger* than builder->data...
	 * converting in place to avoid extra allocation */
	GLubyte *data = (GLubyte *)builder->data;

	if (elem->max_index > 0xFF) {
		const uint base = elem->min_index;
		elem->base_index = base;
		elem->min_index = 0;
		elem->max_index -= base;
		for (uint i = 0; i < index_len; ++i) {
			data[i] = (values[i] == GPU_PRIM_RESTART) ? 0xFF : (GLubyte)(values[i] - base);
		}
	}
	else {
		elem->base_index = 0;
		for (uint i = 0; i < index_len; ++i) {
			data[i] = (GLubyte)(values[i]);
		}
	}
}

static void squeeze_indices_short(GPUIndexBufBuilder *builder, GPUIndexBuf *elem)
{
	const uint *values = builder->data;
	const uint index_len = elem->index_len;

	/* data will never be *larger* than builder->data...
	 * converting in place to avoid extra allocation */
	GLushort *data = (GLushort *)builder->data;

	if (elem->max_index > 0xFFFF) {
		const uint base = elem->min_index;
		elem->base_index = base;
		elem->min_index = 0;
		elem->max_index -= base;
		for (uint i = 0; i < index_len; ++i) {
			data[i] = (values[i] == GPU_PRIM_RESTART) ? 0xFFFF : (GLushort)(values[i] - base);
		}
	}
	else {
		elem->base_index = 0;
		for (uint i = 0; i < index_len; ++i) {
			data[i] = (GLushort)(values[i]);
		}
	}
}

#endif /* GPU_TRACK_INDEX_RANGE */

GPUIndexBuf *GPU_indexbuf_build(GPUIndexBufBuilder *builder)
{
	GPUIndexBuf *elem = MEM_callocN(sizeof(GPUIndexBuf), "GPUIndexBuf");
	GPU_indexbuf_build_in_place(builder, elem);
	return elem;
}

void GPU_indexbuf_build_in_place(GPUIndexBufBuilder *builder, GPUIndexBuf *elem)
{
#if TRUST_NO_ONE
	assert(builder->data != NULL);
#endif
	elem->index_len = builder->index_len;
	elem->use_prim_restart = builder->use_prim_restart;
	elem->ibo_id = 0; /* Created at first use. */

#if GPU_TRACK_INDEX_RANGE
	uint range = index_range(builder->data, builder->index_len, &elem->min_index, &elem->max_index);

	/* count the primitive restart index. */
	if (elem->use_prim_restart) {
		range += 1;
	}

	if (range <= 0xFF) {
		elem->index_type = GPU_INDEX_U8;
		squeeze_indices_byte(builder, elem);
	}
	else if (range <= 0xFFFF) {
		elem->index_type = GPU_INDEX_U16;
		squeeze_indices_short(builder, elem);
	}
	else {
		elem->index_type = GPU_INDEX_U32;
		elem->base_index = 0;
	}
	elem->gl_index_type = convert_index_type_to_gl(elem->index_type);
#endif

	/* Transfer data ownership to GPUIndexBuf.
	 * It will be uploaded upon first use. */
	elem->data = builder->data;
	builder->data = NULL;
	/* other fields are safe to leave */
}

static void indexbuf_upload_data(GPUIndexBuf *elem)
{
	/* send data to GPU */
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, GPU_indexbuf_size_get(elem), elem->data, GL_STATIC_DRAW);
	/* No need to keep copy of data in system memory. */
	MEM_freeN(elem->data);
	elem->data = NULL;
}

void GPU_indexbuf_use(GPUIndexBuf *elem)
{
	if (elem->ibo_id == 0) {
		elem->ibo_id = GPU_buf_alloc();
	}
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elem->ibo_id);
	if (elem->data != NULL) {
		indexbuf_upload_data(elem);
	}
}

void GPU_indexbuf_discard(GPUIndexBuf *elem)
{
	if (elem->ibo_id) {
		GPU_buf_free(elem->ibo_id);
	}
	if (elem->data) {
		MEM_freeN(elem->data);
	}
	MEM_freeN(elem);
}
